Conversation
Enable opening and editing office documents (docx/xlsx/pptx) without a remote OnlyOffice Document Server by using CryptPad's client-side OnlyOffice wrapper and x2t-wasm converter. - Add CryptPadView component with mock server protocol handling - Add x2t-wasm converter for Office <-> OnlyOffice internal format - Add useCryptPadConfig hook for client-side document loading - Add isCryptPadEnabled() helper and feature flag toggle - Support debounced auto-save back to cozy-stack - Split Editor into CryptPad/Server editor variants
In CryptPad mode, saves are done directly by the client via cozy-client, not by the OnlyOffice server. The realtime listener detects the md5sum change and incorrectly warns about external modifications.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Gates Failed
New code is healthy
(2 new files with code health below 10.00)
Enforce critical code health rules
(1 file with Bumpy Road Ahead)
Gates Passed
1 Quality Gates Passed
See analysis details in CodeScene
Reason for failure
| New code is healthy | Violations | Code Health Impact | |
|---|---|---|---|
| CryptPadView.jsx | 4 rules | 7.97 | Suppress |
| converter.js | 1 rule | 9.69 | Suppress |
| Enforce critical code health rules | Violations | Code Health Impact | |
|---|---|---|---|
| CryptPadView.jsx | 1 critical rule | 7.97 | Suppress |
Quality Gate Profile: The Bare Minimum
Install CodeScene MCP: safeguard and uplift AI-generated code. Catch issues early with our IDE extension and CLI tool.
| const CryptPadView = ({ apiUrl, docEditorConfig }) => { | ||
| const [isError, setIsError] = useState(false) | ||
| const docEditorRef = useRef(null) | ||
| const saveTimerRef = useRef(null) | ||
| const client = useClient() | ||
|
|
||
| const { isEditorReady, setIsEditorReady, editorMode, fileId, file } = | ||
| useOnlyOfficeContext() | ||
|
|
||
| // Use a ref to always have the current editorMode in message handlers | ||
| const editorModeRef = useRef(editorMode) | ||
| useEffect(() => { | ||
| editorModeRef.current = editorMode | ||
| }, [editorMode]) | ||
|
|
||
| /** | ||
| * Save the current document back to cozy-stack. | ||
| * Gets the .bin content from the editor, converts it back to the | ||
| * original format (docx/xlsx/pptx), and uploads it. | ||
| */ | ||
| const saveDocument = useCallback(async () => { | ||
| console.log('[CryptPad] saveDocument called, mode:', editorModeRef.current) | ||
| if (editorModeRef.current !== 'edit') return | ||
|
|
||
| const editor = getOOEditor() | ||
| console.log('[CryptPad] editor instance:', editor ? 'found' : 'NOT FOUND') | ||
| if (!editor) { | ||
| console.warn('[CryptPad] Cannot save: editor not available') | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| // Get the document content from the editor. | ||
| // asc_nativeGetFile2 returns a base64-encoded string of the internal | ||
| // DOCY/XLSY/PPTY format. We try multiple methods as fallbacks. | ||
| let exportData = null | ||
| for (const method of ['asc_nativeGetFile2', 'asc_nativeGetFile', 'asc_nativeGetFile3']) { | ||
| try { | ||
| if (typeof editor[method] !== 'function') continue | ||
| const result = editor[method]() | ||
| if (result && (result.byteLength || result.length) > 0) { | ||
| exportData = result | ||
| console.log(`[CryptPad] ${method} returned ${typeof result}, length: ${result.byteLength || result.length}`) | ||
| break | ||
| } | ||
| } catch (e) { | ||
| console.warn(`[CryptPad] ${method} failed:`, e.message) | ||
| } | ||
| } | ||
|
|
||
| if (!exportData) { | ||
| console.warn('[CryptPad] Cannot save: no export method returned data') | ||
| return | ||
| } | ||
|
|
||
| // The editor returns a base64-encoded string of the internal format | ||
| // (starts with "DOCY;", "XLSY;", or "PPTY;"). Decode it to binary. | ||
| let rawData | ||
| if (typeof exportData === 'string') { | ||
| const binaryString = atob(exportData) | ||
| rawData = new Uint8Array(binaryString.length) | ||
| for (let i = 0; i < binaryString.length; i++) { | ||
| rawData[i] = binaryString.charCodeAt(i) | ||
| } | ||
| console.log('[CryptPad] Decoded base64 →', rawData.byteLength, 'bytes, header:', String.fromCharCode(...rawData.slice(0, 12))) | ||
| } else { | ||
| // Typed array from iframe — copy cross-frame | ||
| const len = exportData.byteLength ?? exportData.length ?? 0 | ||
| rawData = new Uint8Array(len) | ||
| for (let i = 0; i < len; i++) { | ||
| rawData[i] = exportData[i] | ||
| } | ||
| } | ||
|
|
||
| const ext = getFileExtension(file.name) | ||
|
|
||
| // Convert internal format back to the original Office format | ||
| const fileData = await convertFromInternal(rawData, ext) | ||
| console.log('[CryptPad] Converted back to', ext, ':', fileData.byteLength, 'bytes') | ||
|
|
||
| // Verify fileData is a valid main-frame Uint8Array | ||
| console.log('[CryptPad] fileData type:', fileData.constructor.name, | ||
| 'instanceof Uint8Array:', fileData instanceof Uint8Array, | ||
| 'byteLength:', fileData.byteLength, | ||
| 'first 4 bytes:', Array.from(fileData.slice(0, 4))) | ||
|
|
||
| const blob = new Blob([fileData], { type: file.mime }) | ||
| console.log('[CryptPad] Blob created:', blob.size, 'bytes, type:', blob.type) | ||
|
|
||
| // Upload back to cozy-stack, overwriting the existing file | ||
| const resp = await client | ||
| .collection(DOCTYPE_FILES) | ||
| .updateFile(blob, { | ||
| fileId, | ||
| name: file.name, | ||
| contentLength: blob.size | ||
| }) | ||
| console.log('[CryptPad] Save response:', JSON.stringify({ | ||
| id: resp?.data?.id || resp?.data?._id, | ||
| size: resp?.data?.size || resp?.data?.attributes?.size, | ||
| rev: resp?.data?._rev || resp?.data?.meta?.rev, | ||
| name: resp?.data?.name || resp?.data?.attributes?.name | ||
| })) | ||
| } catch (error) { | ||
| console.error('[CryptPad] Failed to save document:', error) | ||
| } | ||
| }, [client, file, fileId]) | ||
|
|
||
| /** | ||
| * Schedule a debounced save. Called after each `saveChanges` message | ||
| * from the editor. Waits 2 seconds of inactivity before saving to | ||
| * avoid uploading on every keystroke. | ||
| */ | ||
| const debouncedSave = useCallback(() => { | ||
| if (saveTimerRef.current) { | ||
| clearTimeout(saveTimerRef.current) | ||
| } | ||
| saveTimerRef.current = setTimeout(() => { | ||
| saveTimerRef.current = null | ||
| saveDocument() | ||
| }, 2000) | ||
| }, [saveDocument]) | ||
|
|
||
| /** | ||
| * Initialize the CryptPad-wrapped OnlyOffice editor. | ||
| * The wrapper's api.js replaces DocsAPI.DocEditor with its own class | ||
| * that supports connectMockServer(). | ||
| */ | ||
| const initEditor = useCallback(() => { | ||
| try { | ||
| // CryptPad's wrapper expects window.APP to exist — it sets | ||
| // window.APP.getImageURL during connectMockServer(). | ||
| if (!window.APP) { | ||
| window.APP = {} | ||
| } | ||
|
|
||
| // Intercept /downloadas/ HTTP requests that the editor makes | ||
| // when trying to save through the (non-existent) Document Server. | ||
| patchXHRForDownloadAs() | ||
|
|
||
| // Also patch the iframe's XHR once it's created |
There was a problem hiding this comment.
❌ New issue: Complex Method
CryptPadView (top-level context) has a cyclomatic complexity of 40, threshold = 10
| const getOOEditor = () => { | ||
| const iframe = document.getElementsByName(EDITOR_IFRAME_NAME)[0] | ||
| if (!iframe || !iframe.contentWindow) return null | ||
|
|
||
| const win = iframe.contentWindow | ||
|
|
||
| // OnlyOffice exposes different editor objects depending on document type. | ||
| // Log available objects for debugging. | ||
| const found = | ||
| win.editor || win.editorCell || win.editorPresentation || null | ||
|
|
||
| if (!found) { | ||
| // Look for the editor API on the Asc global (sdkjs exposes it there) | ||
| const api = | ||
| win.Asc && (win.Asc.editor || win.Asc.spreadsheet_api || null) | ||
| if (api) return api | ||
|
|
||
| console.warn('[CryptPad] Editor not found on iframe window. Available keys:', | ||
| Object.keys(win).filter(k => | ||
| k.toLowerCase().includes('editor') || | ||
| k.toLowerCase().includes('asc') || | ||
| k.toLowerCase().includes('api') | ||
| ) | ||
| ) | ||
| } | ||
|
|
||
| return found | ||
| } |
There was a problem hiding this comment.
❌ New issue: Complex Method
getOOEditor has a cyclomatic complexity of 13, threshold = 10
| try { | ||
| if (typeof editor[method] !== 'function') continue | ||
| const result = editor[method]() | ||
| if (result && (result.byteLength || result.length) > 0) { |
There was a problem hiding this comment.
❌ New issue: Complex Conditional
CryptPadView (top-level context) has 1 complex conditionals with 2 branches, threshold = 2
| const CryptPadView = ({ apiUrl, docEditorConfig }) => { | ||
| const [isError, setIsError] = useState(false) | ||
| const docEditorRef = useRef(null) | ||
| const saveTimerRef = useRef(null) | ||
| const client = useClient() | ||
|
|
||
| const { isEditorReady, setIsEditorReady, editorMode, fileId, file } = | ||
| useOnlyOfficeContext() | ||
|
|
||
| // Use a ref to always have the current editorMode in message handlers | ||
| const editorModeRef = useRef(editorMode) | ||
| useEffect(() => { | ||
| editorModeRef.current = editorMode | ||
| }, [editorMode]) | ||
|
|
||
| /** | ||
| * Save the current document back to cozy-stack. | ||
| * Gets the .bin content from the editor, converts it back to the | ||
| * original format (docx/xlsx/pptx), and uploads it. | ||
| */ | ||
| const saveDocument = useCallback(async () => { | ||
| console.log('[CryptPad] saveDocument called, mode:', editorModeRef.current) | ||
| if (editorModeRef.current !== 'edit') return | ||
|
|
||
| const editor = getOOEditor() | ||
| console.log('[CryptPad] editor instance:', editor ? 'found' : 'NOT FOUND') | ||
| if (!editor) { | ||
| console.warn('[CryptPad] Cannot save: editor not available') | ||
| return | ||
| } | ||
|
|
||
| try { | ||
| // Get the document content from the editor. | ||
| // asc_nativeGetFile2 returns a base64-encoded string of the internal | ||
| // DOCY/XLSY/PPTY format. We try multiple methods as fallbacks. | ||
| let exportData = null | ||
| for (const method of ['asc_nativeGetFile2', 'asc_nativeGetFile', 'asc_nativeGetFile3']) { | ||
| try { | ||
| if (typeof editor[method] !== 'function') continue | ||
| const result = editor[method]() | ||
| if (result && (result.byteLength || result.length) > 0) { | ||
| exportData = result | ||
| console.log(`[CryptPad] ${method} returned ${typeof result}, length: ${result.byteLength || result.length}`) | ||
| break | ||
| } | ||
| } catch (e) { | ||
| console.warn(`[CryptPad] ${method} failed:`, e.message) | ||
| } | ||
| } | ||
|
|
||
| if (!exportData) { | ||
| console.warn('[CryptPad] Cannot save: no export method returned data') | ||
| return | ||
| } | ||
|
|
||
| // The editor returns a base64-encoded string of the internal format | ||
| // (starts with "DOCY;", "XLSY;", or "PPTY;"). Decode it to binary. | ||
| let rawData | ||
| if (typeof exportData === 'string') { | ||
| const binaryString = atob(exportData) | ||
| rawData = new Uint8Array(binaryString.length) | ||
| for (let i = 0; i < binaryString.length; i++) { | ||
| rawData[i] = binaryString.charCodeAt(i) | ||
| } | ||
| console.log('[CryptPad] Decoded base64 →', rawData.byteLength, 'bytes, header:', String.fromCharCode(...rawData.slice(0, 12))) | ||
| } else { | ||
| // Typed array from iframe — copy cross-frame | ||
| const len = exportData.byteLength ?? exportData.length ?? 0 | ||
| rawData = new Uint8Array(len) | ||
| for (let i = 0; i < len; i++) { | ||
| rawData[i] = exportData[i] | ||
| } | ||
| } | ||
|
|
||
| const ext = getFileExtension(file.name) | ||
|
|
||
| // Convert internal format back to the original Office format | ||
| const fileData = await convertFromInternal(rawData, ext) | ||
| console.log('[CryptPad] Converted back to', ext, ':', fileData.byteLength, 'bytes') | ||
|
|
||
| // Verify fileData is a valid main-frame Uint8Array | ||
| console.log('[CryptPad] fileData type:', fileData.constructor.name, | ||
| 'instanceof Uint8Array:', fileData instanceof Uint8Array, | ||
| 'byteLength:', fileData.byteLength, | ||
| 'first 4 bytes:', Array.from(fileData.slice(0, 4))) | ||
|
|
||
| const blob = new Blob([fileData], { type: file.mime }) | ||
| console.log('[CryptPad] Blob created:', blob.size, 'bytes, type:', blob.type) | ||
|
|
||
| // Upload back to cozy-stack, overwriting the existing file | ||
| const resp = await client | ||
| .collection(DOCTYPE_FILES) | ||
| .updateFile(blob, { | ||
| fileId, | ||
| name: file.name, | ||
| contentLength: blob.size | ||
| }) | ||
| console.log('[CryptPad] Save response:', JSON.stringify({ | ||
| id: resp?.data?.id || resp?.data?._id, | ||
| size: resp?.data?.size || resp?.data?.attributes?.size, | ||
| rev: resp?.data?._rev || resp?.data?.meta?.rev, | ||
| name: resp?.data?.name || resp?.data?.attributes?.name | ||
| })) | ||
| } catch (error) { | ||
| console.error('[CryptPad] Failed to save document:', error) | ||
| } | ||
| }, [client, file, fileId]) | ||
|
|
||
| /** | ||
| * Schedule a debounced save. Called after each `saveChanges` message | ||
| * from the editor. Waits 2 seconds of inactivity before saving to | ||
| * avoid uploading on every keystroke. | ||
| */ | ||
| const debouncedSave = useCallback(() => { | ||
| if (saveTimerRef.current) { | ||
| clearTimeout(saveTimerRef.current) | ||
| } | ||
| saveTimerRef.current = setTimeout(() => { | ||
| saveTimerRef.current = null | ||
| saveDocument() | ||
| }, 2000) | ||
| }, [saveDocument]) | ||
|
|
||
| /** | ||
| * Initialize the CryptPad-wrapped OnlyOffice editor. | ||
| * The wrapper's api.js replaces DocsAPI.DocEditor with its own class | ||
| * that supports connectMockServer(). | ||
| */ | ||
| const initEditor = useCallback(() => { | ||
| try { | ||
| // CryptPad's wrapper expects window.APP to exist — it sets | ||
| // window.APP.getImageURL during connectMockServer(). | ||
| if (!window.APP) { | ||
| window.APP = {} | ||
| } | ||
|
|
||
| // Intercept /downloadas/ HTTP requests that the editor makes | ||
| // when trying to save through the (non-existent) Document Server. | ||
| patchXHRForDownloadAs() | ||
|
|
||
| // Also patch the iframe's XHR once it's created |
There was a problem hiding this comment.
❌ New issue: Bumpy Road Ahead
CryptPadView (top-level context) has 2 blocks with nested conditional logic. Any nesting of 2 or deeper is considered. Threshold is 2 blocks per function
| @@ -0,0 +1,482 @@ | |||
| import PropTypes from 'prop-types' | |||
There was a problem hiding this comment.
❌ New issue: Overall Code Complexity
This module has a mean cyclomatic complexity of 9.10 across 10 functions. The mean complexity threshold is 4
| export async function convert(inputData, inputFormat, outputFormat) { | ||
| const { module } = await initX2T() | ||
|
|
||
| // Use unique file paths per conversion to avoid race conditions | ||
| const conversionId = Math.random().toString(36).substring(2, 10) | ||
| const inputExt = DOC_TYPE_EXT[inputFormat] || inputFormat | ||
| const outputExt = DOC_TYPE_EXT[outputFormat] || outputFormat | ||
| const inputPath = `/working/input-${conversionId}.${inputExt}` | ||
| const outputPath = `/working/output-${conversionId}.${outputExt}` | ||
| const paramsPath = `/working/params-${conversionId}.xml` | ||
|
|
||
| // Write input file to Emscripten virtual FS | ||
| module.FS.writeFile(inputPath, inputData) | ||
|
|
||
| // Write conversion params | ||
| const params = buildParamsXML(inputPath, outputPath, inputFormat, outputFormat) | ||
| module.FS.writeFile(paramsPath, params) | ||
|
|
||
| // Run conversion | ||
| const result = module.ccall('main1', 'number', ['string'], [paramsPath]) | ||
|
|
||
| if (result !== 0) { | ||
| // Clean up | ||
| try { module.FS.unlink(inputPath) } catch (e) { /* ignore */ } | ||
| try { module.FS.unlink(paramsPath) } catch (e) { /* ignore */ } | ||
| throw new Error(`x2t conversion failed with code ${result}`) | ||
| } | ||
|
|
||
| // Read output — copy to main frame to avoid cross-frame typed array issues | ||
| const iframeOutput = module.FS.readFile(outputPath) | ||
| const outputData = new Uint8Array(iframeOutput.length) | ||
| outputData.set(iframeOutput) | ||
|
|
||
| // Clean up virtual FS | ||
| try { module.FS.unlink(inputPath) } catch (e) { /* ignore */ } | ||
| try { module.FS.unlink(outputPath) } catch (e) { /* ignore */ } | ||
| try { module.FS.unlink(paramsPath) } catch (e) { /* ignore */ } | ||
|
|
||
| return outputData | ||
| } |
There was a problem hiding this comment.
❌ New issue: Complex Method
convert has a cyclomatic complexity of 10, threshold = 9
No description provided.